/*
 *  Copyright 2012 Anton Van Zyl. http://code.google.com/p/java-swiss-knife/
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package com.knife.web.threading;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.codec.binary.Hex;

import com.knife.web.threading.exception.InvalidRequestTokenException;

/**
 * This is a thread lock that only allows one thread to enter and ignoring extra
 * threads until the first thread is completed and then letting the user
 * continue to the result page.
 * 
 * <br/>
 * <br/>
 * Please visit <a
 * href="http://code.google.com/p/java-swiss-knife/">Java-Swiss-Knife</a> and
 * comment, rate, contribute or raise a issue/enhancement for my library. <br/>
 * 
 * @author Anton Van Zyl
 * 
 */
public class DoubleSubmitLock {

	private static final String SESSION_REQ_ID = "session_req_id";

	private static SecureRandom secureRandom;

	private final String uniqueRequestId;
	private final Lock controlLock = new ReentrantLock();
	private final Lock mainLock = new ReentrantLock();
	private final Lock subLock = new ReentrantLock();
	private final Condition cond = subLock.newCondition();
	private boolean finished;

	private String jspReturnPage;
	private Map<String, Object> jspReturnPageData = new HashMap<String, Object>();

	static {
		try {
			secureRandom = SecureRandom.getInstance("SHA1PRNG");
		} catch (NoSuchAlgorithmException e) {
			System.out.println("getUniqueRequestIdenitfier: " + e.getMessage());
			e.printStackTrace();
		}
	}

	public DoubleSubmitLock(String uniqueRequestId) {
		this.uniqueRequestId = uniqueRequestId;
	}

	public boolean isSameRequestId(String requestId) {
		return uniqueRequestId.equalsIgnoreCase(requestId);
	}

	public boolean tryGetLock(String errorJspPage, Map<String, Object> jspErrorPageData) throws InterruptedException {

		// obtain controlLock so we can also get subLock before the main thread
		// can signal release - and cause us to wait forever
		controlLock.lock();

		if (mainLock.tryLock()) {
			System.out.println("Lock obtained [id=" + uniqueRequestId + "]");
			jspReturnPage = errorJspPage;
			jspReturnPageData = jspErrorPageData;
			controlLock.unlock();
			return true;
		} else {
			try {
				System.out.println("Lock NOT obtained; blocking [id=" + uniqueRequestId + "]");
				subLock.lock();
				// we have the subLock; can release controlLock now before we
				// enter the wait
				controlLock.unlock();
				cond.await();
				System.out.println("Blocking released [id=" + uniqueRequestId + "]");
			} finally {
				subLock.unlock();
			}
		}
		return false;
	}

	public void unlock() {
		unlock(true);
	}

	public void unlock(boolean completed) {

		controlLock.lock();
		System.out.println("Lock released [id=" + uniqueRequestId + "; completed=" + completed + "]");
		try {
			finished = completed;
			subLock.lock();
			try {
				cond.signalAll();
			} finally {
				subLock.unlock();
			}
		} finally {
			controlLock.unlock();
		}
	}

	/**
	 * @return the current JSP page
	 */
	public String getJspReturnPage() {
		return this.jspReturnPage;
	}

	/**
	 * @return The data the JSP page need to render correctly
	 */
	public Map<String, Object> getJspReturnPageData() {
		return this.jspReturnPageData;
	}

	/**
	 * 
	 * @param jspReturnPage
	 */
	public void setJspReturnPage(String jspReturnPage) {
		this.jspReturnPage = jspReturnPage;
	}

	/**
	 * 
	 * @param jspReturnPageData
	 */
	public void setJspReturnPageData(Map<String, Object> jspReturnPageData) {
		this.jspReturnPageData = jspReturnPageData;
	}

	/**
	 * @return Test if request is processed
	 */
	public boolean isRequestAlreadyProcessed() {
		controlLock.lock();
		try {
			return finished;
		} finally {
			controlLock.unlock();
		}
	}

	/**
	 * Generates a random Token and adds it to the session to be used in
	 * validating incoming requests.<br/>
	 * Use the validateUniqueRequestIdentifier method to validate the key value
	 * against the session.<br/>
	 */
	public static String createDoubleSubmitLock(HttpServletRequest request, String callerId) {
		byte[] bytes = new byte[32];
		secureRandom.nextBytes(bytes);
		String uniqueRequestId = Hex.encodeHexString(bytes);

		System.out.println("Created lock [id=" + uniqueRequestId + "]");

		request.getSession().setAttribute(getSessionId(callerId), new DoubleSubmitLock(uniqueRequestId));
		return uniqueRequestId;
	}

	private static String getSessionId(String callerId) {
		return SESSION_REQ_ID + ":" + callerId;
	}

	/**
	 * This will validate the key in the session and if invalid throw an
	 * exception to be handled and send the user to a generic double submit
	 * page.<br/>
	 * 
	 * @throws InvalidRequestTokenException
	 */
	public static DoubleSubmitLock getDoubleSubmitLock(HttpServletRequest request, String callerId, String requestId) throws InvalidRequestTokenException {

		DoubleSubmitLock dsl = (DoubleSubmitLock) request.getSession().getAttribute(getSessionId(callerId));

		if ((dsl == null) || (!dsl.isSameRequestId(requestId))) {
			String msg = "DoubleSubmitLock not bound to session [callerId=" + callerId + "; requestId=" + requestId + "]";
			System.out.println(msg);
			throw new InvalidRequestTokenException(msg);
		}
		return dsl;
	}
}
